package avicit.bdp.dds.server.worker.task.sql;

import avicit.bdp.common.datasource.BaseDataSource;
import avicit.bdp.common.datasource.DataSourceFactory;
import avicit.bdp.common.service.dto.DataSourceDTO;
import avicit.bdp.common.utils.SpringApplicationContext;
import avicit.bdp.common.utils.database.DBUtils;
import avicit.bdp.dds.dispatch.enums.TaskTimeoutStrategy;
import avicit.bdp.dds.dispatch.process.Property;
import avicit.bdp.dds.dispatch.task.AbstractParameters;
import avicit.bdp.dds.dispatch.task.sql.SqlBinds;
import avicit.bdp.dds.dispatch.task.sql.SqlParameters;
import avicit.bdp.dds.dispatch.task.sql.SqlType;
import avicit.bdp.dds.common.Constants;
import avicit.bdp.dds.common.utils.ParameterUtils;
import avicit.bdp.dds.dao.AlertDao;
import avicit.bdp.dds.server.entity.SQLTaskExecutionContext;
import avicit.bdp.dds.server.entity.TaskExecutionContext;
import avicit.bdp.dds.server.utils.ParamUtils;
import avicit.bdp.dds.server.utils.UDFUtils;
import avicit.bdp.dds.server.worker.task.AbstractTask;
import avicit.bdp.dds.service.process.ProcessService;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * sql task
 */
public class SqlTask extends AbstractTask {

    /**
     * sql parameters
     */
    private SqlParameters sqlParameters;
    /**
     * alert dao
     */
    private AlertDao alertDao;
    /**
     * base datasource
     */
    private BaseDataSource baseDataSource;

    /**
     * taskExecutionContext
     */
    private TaskExecutionContext taskExecutionContext;

    private final ProcessService processService = SpringApplicationContext.getBean(ProcessService.class);

    /**
     * default query sql limit
     */
    private static final int LIMIT = 100;

    public SqlTask(TaskExecutionContext taskExecutionContext, Logger logger) {
        super(taskExecutionContext, logger);

        this.taskExecutionContext = taskExecutionContext;

        logger.info("sql task params {}", taskExecutionContext.getTaskParams());
        this.sqlParameters = JSONObject.parseObject(taskExecutionContext.getTaskParams(), SqlParameters.class);

        DataSourceDTO datasource = processService.findDataSourceById(this.sqlParameters.getDatasource());
        this.sqlParameters.setType(datasource.getDatasourceType());

        if (!sqlParameters.checkParameters()) {
            throw new RuntimeException("sql task params is not valid");
        }

        this.alertDao = SpringApplicationContext.getBean(AlertDao.class);
    }

    @Override
    public void handle() throws Exception {
        // set the name of the current thread
        String threadLoggerInfoName = String.format(Constants.TASK_LOG_INFO_FORMAT, taskExecutionContext.getTaskAppId());
        Thread.currentThread().setName(threadLoggerInfoName);

        logger.info("Full sql parameters: {}", sqlParameters);
        logger.info("sql type : {}, datasource : {}, sql : {} , localParams : {},udfs : {},showType : {},connParams : {}",
                sqlParameters.getType(),
                sqlParameters.getDatasource(),
                sqlParameters.getSql(),
                sqlParameters.getLocalParams(),
                sqlParameters.getUdfs(),
                sqlParameters.getShowType(),
                sqlParameters.getConnParams());
        try {
            SQLTaskExecutionContext sqlTaskExecutionContext = taskExecutionContext.getSqlTaskExecutionContext();
            // load class
            //DataSourceFactory.loadClass(sqlParameters.getType().toLowerCase());

            // get datasource
            baseDataSource = DataSourceFactory.getDatasource(sqlParameters.getType().toLowerCase(),
                    sqlTaskExecutionContext.getConnectionParams());

            // ready to execute SQL and parameter entity Map
            SqlBinds mainSqlBinds = getSqlAndSqlParamsMap(sqlParameters.getSql());
            List<SqlBinds> preStatementSqlBinds = Optional.ofNullable(sqlParameters.getPreStatements())
                    .orElse(new ArrayList<>())
                    .stream()
                    .map(this::getSqlAndSqlParamsMap)
                    .collect(Collectors.toList());
            List<SqlBinds> postStatementSqlBinds = Optional.ofNullable(sqlParameters.getPostStatements())
                    .orElse(new ArrayList<>())
                    .stream()
                    .map(this::getSqlAndSqlParamsMap)
                    .collect(Collectors.toList());

            List<String> createFuncs = UDFUtils.createFuncs(sqlTaskExecutionContext.getUdfFuncTenantCodeMap(), taskExecutionContext.getProjectId(), logger);

            // execute sql task
            executeFuncAndSql(mainSqlBinds, preStatementSqlBinds, postStatementSqlBinds, createFuncs);

            setExitStatusCode(Constants.EXIT_CODE_SUCCESS);

        } catch (Exception e) {
            setExitStatusCode(Constants.EXIT_CODE_FAILURE);
            logger.error("sql task error info: " + e.getMessage());
            throw e;
        }
    }

    /**
     * ready to execute SQL and parameter entity Map
     *
     * @return
     */
    private SqlBinds getSqlAndSqlParamsMap(String sql) {
        if (sql == null) {
            sql = "";
        }
        Map<Integer, Property> sqlParamsMap = new HashMap<>(16);
        StringBuilder sqlBuilder = new StringBuilder();

        // find process instance by task id

        Map<String, Property> paramsMap = ParamUtils.convert(ParamUtils.getUserDefParamsMap(taskExecutionContext.getDefinedParams()),
                taskExecutionContext.getDefinedParams(),
                sqlParameters.getLocalParametersMap(),
                taskExecutionContext.getCmdTypeIfComplement(),
                taskExecutionContext.getScheduleTime());

        // spell SQL according to the final user-defined variable
        if (paramsMap == null) {
            sqlBuilder.append(sql);
            return new SqlBinds(sqlBuilder.toString(), sqlParamsMap);
        }

        if (StringUtils.isNotEmpty(sqlParameters.getTitle())) {
            String title = ParameterUtils.convertParameterPlaceholders(sqlParameters.getTitle(),
                    ParamUtils.convert(paramsMap));
            logger.info("SQL title : {}", title);
            sqlParameters.setTitle(title);
        }
        //new
        //replace variable TIME with $[YYYYmmddd...] in sql when history run job and batch complement job
        sql = ParameterUtils.replaceScheduleTime(sql, taskExecutionContext.getScheduleTime());
        // special characters need to be escaped, ${} needs to be escaped
        String rgex = "['\"]*\\$\\{(.*?)\\}['\"]*";
        setSqlParamsMap(sql, rgex, sqlParamsMap, paramsMap);

        // replace the ${} of the SQL statement with the Placeholder
        String formatSql = sql.replaceAll(rgex, "?");
        sqlBuilder.append(formatSql);

        // print replace sql
        printReplacedSql(sql, formatSql, rgex, sqlParamsMap);
        return new SqlBinds(sqlBuilder.toString(), sqlParamsMap);
    }

    @Override
    public AbstractParameters getParameters() {
        return this.sqlParameters;
    }

    /**
     * execute function and sql
     *
     * @param mainSqlBinds        main sql binds
     * @param preStatementsBinds  pre statements binds
     * @param postStatementsBinds post statements binds
     * @param createFuncs         create functions
     */
    public void executeFuncAndSql(SqlBinds mainSqlBinds,
                                  List<SqlBinds> preStatementsBinds,
                                  List<SqlBinds> postStatementsBinds,
                                  List<String> createFuncs) throws Exception {
        Connection connection = null;
        PreparedStatement stmt = null;
        ResultSet resultSet = null;
        try {
            // if upload resource is HDFS and kerberos startup
            //CommonUtils.loadKerberosConf();
            // create connection
            connection = createConnection();
            // create temp function
            if (CollectionUtils.isNotEmpty(createFuncs)) {
                createTempFunction(connection, createFuncs);
            }

            // pre sql
            preSql(connection, preStatementsBinds);
            stmt = prepareStatementAndBind(connection, mainSqlBinds);
            //自动判断sql是否是查询，如果是查询，则把结果输出到日志中
            if (mainSqlBinds.getSql() != null && (mainSqlBinds.getSql().toLowerCase().trim().startsWith("select")
                    || mainSqlBinds.getSql().toLowerCase().trim().startsWith("show"))) {
                sqlParameters.setSqlType(SqlType.QUERY.ordinal());
            } else {
                sqlParameters.setSqlType(SqlType.NON_QUERY.ordinal());
            }
            // decide whether to executeQuery or executeUpdate based on sqlType
            if (sqlParameters.getSqlType() == SqlType.QUERY.ordinal()) {
                // query statements need to be convert to JsonArray and inserted into Alert to send
                resultSet = stmt.executeQuery();
                resultProcess(resultSet);

            } else if (sqlParameters.getSqlType() == SqlType.NON_QUERY.ordinal()) {
                // non query statement
                stmt.execute();
            }

            postSql(connection, postStatementsBinds);

        } catch (Exception e) {
            logger.error("execute sql error", e.getMessage());
            throw e;
        } finally {
            DBUtils.closeResource(connection, stmt, resultSet);
        }
    }

    /**
     * result process
     *
     * @param resultSet resultSet
     * @throws Exception
     */
    private void resultProcess(ResultSet resultSet) throws Exception {
        JSONArray resultJSONArray = new JSONArray();
        ResultSetMetaData md = resultSet.getMetaData();
        int num = md.getColumnCount();

        int rowCount = 0;
        logger.info("查询结果输出， 最多输出100条记录。");
        while (rowCount < LIMIT && resultSet.next()) {
            JSONObject mapOfColValues = new JSONObject();
            for (int i = 1; i <= num; i++) {
                mapOfColValues.put(md.getColumnName(i), resultSet.getObject(i));
            }
            resultJSONArray.add(mapOfColValues);
            rowCount++;
            logger.info("\t" + JSONObject.toJSONString(mapOfColValues, JSONWriter.Feature.WriteMapNullValue));
        }

    }

    /**
     * pre sql
     *
     * @param connection         connection
     * @param preStatementsBinds preStatementsBinds
     */
    private void preSql(Connection connection,
                        List<SqlBinds> preStatementsBinds) throws Exception {
        for (SqlBinds sqlBind : preStatementsBinds) {
            if (sqlBind == null || StringUtils.isBlank(sqlBind.getSql())) {
                continue;
            }
            try (PreparedStatement pstmt = prepareStatementAndBind(connection, sqlBind)) {
                int result = pstmt.executeUpdate();
                logger.info("pre statement execute result: {}, for sql: {}", result, sqlBind.getSql());

            }
        }
    }

    /**
     * post psql
     *
     * @param connection          connection
     * @param postStatementsBinds postStatementsBinds
     * @throws Exception
     */
    private void postSql(Connection connection,
                         List<SqlBinds> postStatementsBinds) throws Exception {
        for (SqlBinds sqlBind : postStatementsBinds) {
            if (sqlBind == null || StringUtils.isBlank(sqlBind.getSql())) {
                continue;
            }
            try (PreparedStatement pstmt = prepareStatementAndBind(connection, sqlBind)) {
                int result = pstmt.executeUpdate();
                logger.info("post statement execute result: {},for sql: {}", result, sqlBind.getSql());
            }
        }
    }

    /**
     * create temp function
     *
     * @param connection  connection
     * @param createFuncs createFuncs
     * @throws Exception
     */
    private void createTempFunction(Connection connection,
                                    List<String> createFuncs) throws Exception {
        try (Statement funcStmt = connection.createStatement()) {
            for (String createFunc : createFuncs) {
                logger.info("param create function sql: {}", createFunc);
                funcStmt.execute(createFunc);
            }
        }
    }

    /**
     * create connection
     *
     * @return connection
     * @throws Exception
     */
    private Connection createConnection() throws Exception {
        // if param , load connection params if exists
//        Connection connection = null;
//        if (sqlParameters.getType().equalsIgnoreCase(HIVE.getDescp())) {
//            Properties paramProp = new Properties();
//            paramProp.setProperty(USER, baseDataSource.getUser());
//            paramProp.setProperty(PASSWORD, DESUtils.decrypt(baseDataSource.getPassword()));
//            Map<String, String> connParamMap = CollectionUtils.stringToMap(sqlParameters.getConnParams(),
//                    SEMICOLON,
//                    HIVE_CONF);
//            paramProp.putAll(connParamMap);
//
//            connection = DriverManager.getConnection(baseDataSource.getJdbcUrl(),
//                    paramProp);
//        }else{
//            connection = DriverManager.getConnection(baseDataSource.getJdbcUrl(),
//                    baseDataSource.getUser(),
//                    DESUtils.decrypt(baseDataSource.getPassword()));
//        }
        return DBUtils.buildConnection(baseDataSource);
    }


    /**
     * preparedStatement bind
     *
     * @param connection
     * @param sqlBinds
     * @return
     * @throws Exception
     */
    private PreparedStatement prepareStatementAndBind(Connection connection, SqlBinds sqlBinds) throws Exception {
        // is the timeout set
        boolean timeoutFlag = TaskTimeoutStrategy.of(taskExecutionContext.getTaskTimeoutStrategy()) == TaskTimeoutStrategy.FAILED ||
                TaskTimeoutStrategy.of(taskExecutionContext.getTaskTimeoutStrategy()) == TaskTimeoutStrategy.WARNFAILED;
        PreparedStatement stmt = connection.prepareStatement(sqlBinds.getSql());
        if (timeoutFlag) {
            stmt.setQueryTimeout(taskExecutionContext.getTaskTimeout());
        }
        Map<Integer, Property> params = sqlBinds.getParamsMap();
        if (params != null) {
            for (Map.Entry<Integer, Property> entry : params.entrySet()) {
                Property prop = entry.getValue();
                ParameterUtils.setInParameter(entry.getKey(), stmt, prop.getType(), prop.getValue());
            }
        }
        logger.info("prepare statement replace sql : {} ", stmt);
        return stmt;
    }


    /**
     * regular expressions match the contents between two specified strings
     *
     * @param content        content
     * @param rgex           rgex
     * @param sqlParamsMap   sql params map
     * @param paramsPropsMap params props map
     */
    public void setSqlParamsMap(String content, String rgex, Map<Integer, Property> sqlParamsMap, Map<String, Property> paramsPropsMap) {
        if (StringUtils.isBlank(content)) {
            return;
        }
        Pattern pattern = Pattern.compile(rgex);
        Matcher m = pattern.matcher(content);
        int index = 1;

        while (m.find()) {
            String paramName = m.group(1);
            Property prop = paramsPropsMap.get(paramName);

            sqlParamsMap.put(index, prop);
            index++;
        }
    }

    /**
     * print replace sql
     *
     * @param content      content
     * @param formatSql    format sql
     * @param rgex         rgex
     * @param sqlParamsMap sql params map
     */
    public void printReplacedSql(String content, String formatSql, String rgex, Map<Integer, Property> sqlParamsMap) {
        //parameter print style
        logger.info("after replace sql , preparing : {}", formatSql);
        StringBuilder logPrint = new StringBuilder("replaced sql , parameters:");
        for (int i = 1; i <= sqlParamsMap.size(); i++) {
            logPrint.append(sqlParamsMap.get(i).getValue()).append("(").append(sqlParamsMap.get(i).getType()).append(")");
        }
        logger.info("Sql Params are {}", logPrint);
    }
}
